<?php
/**
 * Created by PhpStorm.
 * User: ari
 * Date: 12/25/2014
 * Time: 2:36 PM
 */
namespace BC\Games\Holdem;

use BC\Config;
use BC\Games\Holdem\Hands\IPokerHand;
use BC\Games\Holdem\Hands\PokerHands;
use BC\Socket\SocketRequest;
use BC\Socket\WebSocket;
use BC\User\Session\UserSession;
use CPath\Render\HTML\Element\Form\HTMLInputField;
use CPath\Render\HTML\Element\Form\HTMLSelectField;
use CPath\Request\CLI\CLIRequest;
use CPath\Request\Exceptions\RequestException;
use CPath\Request\Executable\IExecutable;
use CPath\Request\Form\IFormRequest;
use CPath\Request\IRequest;
use CPath\Request\Log\ILogListener;
use CPath\Request\Session\ISessionRequest;
use CPath\Response\IResponse;
use CPath\Response\Response;
use CPath\UnitTest\ITestable;
use CPath\UnitTest\IUnitTestRequest;
use CPath\UnitTest\UnitTestRequestWrapper;

if(!empty($argv) && $argv[0] === __FILE__)
	require_once '../../SiteIndex.php';

class HoldemRound implements ILogListener, ITestable, IExecutable
{
	const PARAM_TABLE   = 'id';
	const PARAM_ROUND   = 'round-id';
	const PARAM_COMMAND = 'command';
	const PARAM_AMOUNT  = 'amount';
	const PARAM_CURRENT_BET  = 'current-bet';
	const PARAM_SHOW    = 'show';

	const GROUP_BOARD = 'board';
	const GROUP_BURN = 'burn';

	const EVENT_BUTTON        = 'button';

	const EVENT_DEAL          = 'deal';
	const EVENT_ROUND_BEGIN   = 'round-begin';
	const EVENT_WIN_HAND      = 'win-hand';

	const EVENT_CHECK         = 'check';
	const EVENT_READY         = 'ready';
	const EVENT_FOLD          = 'fold';
	const EVENT_CALL          = 'call';
	const EVENT_BET           = 'bet';
	const EVENT_RAISE         = 'raise';
	const EVENT_ALLIN         = 'all-in';
	const EVENT_MESSAGE       = 'msg';

	const EVENT_SIDE_POT      = 'sidepot';

	const FLAG_STARTED      = 0x01;

	const PATH_FORMAT       = 'games/holdem/table/%s/round.log';

	/** @var ILogListener[] */
	private $mLogListeners = array();
	/** @var Deck */
	private $mDeck = null;

	private $mSidePots = array();
	private $mPaidOut = 0;

	private $mTable;

	private $mButtonID = null;

	private $mOffset;

	private $mStarted = false;
	private $mWhoWon = array();

	public function __construct(IRequest $Request, HoldemTable $Table, $offset=0) {
		$this->mDeck = new Deck();
		$this->mTable = $Table;
		$this->mOffset = $offset;

		$path = sprintf(HoldemTable::PATH_FORMAT, $Table->getTableID()) . HoldemTable::ROUND_LOG;
		$path = Config::getContentPath($path);
		if(file_exists($path)) {
			$handle      = fopen($path, 'r');
			fseek($handle, $offset);
			while ($line = fgets($handle))
				if($line = rtrim($line))
					$this->readLine($Request, explode(' ', $line));
			fclose($handle);
		}
	}


	public function readLogCallback(\Closure $callback = null) {
		$path = sprintf(HoldemTable::PATH_FORMAT, $this->getTableID()) . HoldemTable::ROUND_LOG;
		$path = Config::getContentPath($path);
		if(file_exists($path)) {
			$handle      = fopen($path, 'r');

			fseek($handle, $this->mOffset);
			while ($line = fgets($handle)) {
				if(true === $callback($line))
					break;
			}
			fclose($handle);
		}
 	}

	public function ready(IRequest $Request, $playerID) {
		$this->writeLine($Request, self::EVENT_READY, $playerID);
	}

	public function message(IRequest $Request, $playerID, $message) {
		$message = preg_replace('/[^a-zA-Z0-9 _!@#$%^&*();{}-]/', '_', $message);
		$this->writeLine($Request, self::EVENT_MESSAGE, $playerID, $message);
	}

	protected function readLog(IRequest $Request) {
		$path        = self::getLogPath($this->getTableID());
		$handle      = fopen($path, 'r');
		fseek($handle, $this->mOffset);
		while ($line = fgets($handle))
			$this->readLine($Request, explode(' ', rtrim($line)));
		fclose($handle);
	}

	public function getTableID() {
		return $this->mTable->getTableID();
	}

	public function getTable() {
		return $this->mTable;
	}

	/**
	 * Execute a command and return a response. Does not render
	 * @param null|IRequest $Request
	 * @throws \CPath\Request\Exceptions\RequestException
	 * @return IResponse the execution response
	 */
	function execute(IRequest $Request) {
		$commands = array();

		$playerID = null;
		$UserSession = null;
		if($Request instanceof ISessionRequest && UserSession::hasSession($Request)) {
			$UserSession = UserSession::fromSession($Request);
			$playerID = $UserSession->getPGPInfo($Request)->getUserID();
		}

		$isSitting = $this->hasPlayer($playerID);

		$Player = null;
		if($this->mDeck->getDrawCount() > 0) {
			if($isSitting) {
				$Player = $this->getPlayer($playerID);
				if ($isSitting) {
					if ($this->hasMove($playerID)) {
						$commands[] = self::EVENT_RAISE;
						$commands[] = self::EVENT_BET;
						$commands[] = self::EVENT_CALL;
					}
					if (!$Player->hasFolded()) {
						$commands[] = self::EVENT_FOLD;
					}
					if (!$Player->hasChecked()) {
						$commands[] = self::EVENT_CHECK;
					}
				}
			}
		}

		if (!$UserSession)
			throw new RequestException("Login required");

		$PCommand = new HTMLSelectField(self::PARAM_COMMAND, null, $commands);

		if (!$Request instanceof IFormRequest)
			return $PCommand;

		$cmd      = $PCommand($Request);

		switch ($cmd) {

			case self::EVENT_CHECK:
				$this->check($Request, $playerID);
				return new Response("Check executed successfully");

			case self::EVENT_FOLD:
				$PShow = new HTMLInputField("Show cards?");
				$this->fold($Request, $playerID, $PShow($Request) === '1' ? true : false);
				return new Response("Fold executed successfully");

			case self::EVENT_BET:
				$amount  = $Request[self::PARAM_AMOUNT];

				$this->betToAmount($Request, $playerID, $amount);
				return new Response("Bet executed successfully");

			case self::EVENT_CALL:
				$this->call($Request, $playerID);
				return new Response("Call executed successfully");

			case self::EVENT_ALLIN:
				$this->allIn($Request, $playerID);
				return new Response("All-in executed successfully");

			case self::EVENT_RAISE:
				$amount  = $Request[self::PARAM_AMOUNT];

				$this->raise($Request, $playerID, $amount);
				return new Response("Call executed successfully");

			default:
				throw new RequestException("Invalid command: " . $cmd);
		}
	}

	public function getPath() {
		return self::getLogPath($this->getTableID());
	}

	public function hasStarted(&$whoWon=array()) {
		$whoWon = $this->mWhoWon;
		return $this->mStarted;
	}

	public function waitingForReadyPlayers(&$reason=null) {
		if($this->hasStarted())
			throw new \InvalidArgumentException("Round has already started");
		$notReadyIDs = array();
		foreach($this->getPlayers() as $Player)
			if(!$Player->isReady())
				$notReadyIDs[] = $Player->getPlayerID();

		if(sizeof($notReadyIDs) > 0) {
			$reason = 'Waiting for player(s) to start: ' . implode(', ', $notReadyIDs);
			return true;
		}
		return null;
	}

	public function waitingForPlayerID(&$reason=null) {
		foreach($this->getPlayers() as $Player)
			if($this->hasMove($Player->getPlayerID(), $reason))
				return $Player->getPlayerID();
		return null;
	}

	public function getUnfoldedPlayerCount() {
		$count = 0;
		foreach($this->getPlayers() as $Player)
			if(!$Player->hasFolded())
				$count++;
		return $count;
	}

	public function getDealerButtonPlayer(&$pos = null) {
		foreach($this->getTable()->getPlayers() as $i=>$Player) {
			if ($Player->getPlayerID() === $this->mButtonID) {
				$pos = $i;

				return $Player;
			}
		}
		throw new \Exception("No button player found");
	}

	public function getSmallBlindPlayer() {
		$i = null;
		$this->getDealerButtonPlayer($i);
		$p = $i-1;
		$Players = $this->getPlayers();
		if($p < 0)
			$p += sizeof($Players);
		return $Players[$p];
	}

	public function getBigBlindPlayer() {
		$i = null;
		$this->getDealerButtonPlayer($i);
		$p = $i-2;
		$Players = $this->getPlayers();
		if($p < 0)
			$p += sizeof($Players);
		return $Players[$p];
	}

	public function getCurrentPotBalance() {
		$balance = 0;
		foreach($this->getPlayers() as $Player)
			$balance += $Player->getBetAmount();
		return $balance;
	}

	public function getCurrentBetAmount() {
		$bet = 0;
		foreach($this->getPlayers() as $Player)
			if(!$Player->hasFolded() && $Player->getBetAmount() > $bet)
				$bet = $Player->getBetAmount();

		return $bet;
	}

	public function hasMove($playerID, &$reason=null) {
		if(!$this->hasStarted()) {
			throw new \InvalidArgumentException("Cant ask for move before round starts"); }

		$Player = $this->getPlayer($playerID);
		if($Player->hasFolded()) {
			$reason = $Player->getPlayerID() . ' has folded';
			return false;
		}

		$currentBet = $this->getCurrentBetAmount();

		if($currentBet < $Player->getBetAmount()) {
			$reason = "Waiting on " . $Player->getPlayerID() . " to bet or fold";
			return true;
		}

		if($currentBet == $Player->getBetAmount()) {
			$waiting = 0;
			$unfolded = 0;
			foreach($this->getPlayers() as $Player2) {
				if(!$Player2->hasFolded()) {
					$unfolded++;
					if ($Player2->getBetAmount() < $currentBet) {
						$waiting++;
					}
				}
			}
			if($unfolded === 1) {
				$reason = $Player->getPlayerID() . ' is the last remaining player';
				return false;
			}

			if($waiting) {
				$reason = "Waiting on {$waiting} player(s) with lower bets to bet or fold";
				return false;
			}

			if($Player->hasChecked()) {
				$reason = "Player has checked";
				return false;
			}

//			if(in_array($currentBet, $this->getTable()->getBlinds())) {
//				$reason = "Player may bet beyond the blinds";
//				return true;
//			}

			if(!$Player->hasChecked() && !$Player->hasBet()) {
				$reason = "Waiting on player " . $Player->getPlayerID() . " to check";
				return true;
			}

			$reason = "Player has highest bet";
			return false;
		}

		if($Player->getBetAmount() === $Player->getBalance()) {
			$reason = "Player " . $Player->getPlayerID() . " is all-in";
			return false;
		}

		$reason = "Waiting on " . $Player->getPlayerID() . " to check, bet or fold";
		return true;
	}

	protected function generatePlayerPassphrase($sessionFingerprint, $masterPassphrase) {
		return crypt($sessionFingerprint, $masterPassphrase);
	}

	public function setDealerButton(IRequest $Request, $getUserID) {
		$this->getPlayer($getUserID);
		$this->writeLine($Request, self::EVENT_BUTTON, $getUserID);
	}
//
//	public function setBlinds(IRequest $Request, $smallBlind, $bigBlind) {
//		$this->writeLine($Request, self::EVENT_BLINDS, $smallBlind, $bigBlind);
//	}

	public function bet(IRequest $Request, $playerID, $bet) {
		$bet += $this->getPlayer($playerID)->getBetAmount();
		$this->betToAmount($Request, $playerID, $bet);
	}

	public function betToAmount(IRequest $Request, $playerID, $betToAmount) {
		$this->writeLine($Request, self::EVENT_BET, $playerID, $betToAmount);
	}

	public function raise(IRequest $Request, $playerID, $raiseAmount) {
		$this->writeLine($Request, self::EVENT_RAISE, $playerID, $raiseAmount);
	}

	public function allIn(IRequest $Request, $playerID) {
		$betToAmount = $this->getMaximumBet($playerID);
		$this->writeLine($Request, self::EVENT_ALLIN, $playerID, $betToAmount);
	}

	public function call(IRequest $Request, $playerID) {
		$callToAmount = $this->getCurrentBetAmount();
		$Player = $this->getPlayer($playerID);
		if($callToAmount > $Player->getBalance()) {
			$this->writeLine($Request, self::EVENT_ALLIN, $playerID, $Player->getBalance());
		} else {
			$this->writeLine($Request, self::EVENT_CALL, $playerID, $callToAmount);
		}
	}

	public function fold(IRequest $Request, $playerID, $show=false) {
		if($show) {
			list($card1, $card2) = $this->getPlayer($playerID)->getDrawnCards();
			$this->writeLine($Request, self::EVENT_FOLD, $playerID, $card1, $card2);
		} else {
			$this->writeLine($Request, self::EVENT_FOLD, $playerID);
		}
	}

//	public function show(IRequest $Request, $playerID) {
//		$cards = $this->mDeck->getDrawnCards($playerID);
//		$this->writeLine($Request, self::EVENT_SHOW, $playerID, $cards[0], $cards[1]);
//		$this->log($playerID . ' shows cards: ' . Cards::printCards($cards));
//	}

	public function getEncryptedPlayerCards(IRequest $Request, $playerID) {
		$Player = $this->getPlayer($playerID);
		if(!$this->mDeck->hasGroup($playerID))
			return array();
		$cards = $this->mDeck->getDrawnCards($playerID);
		$encryptedCards = array();
		foreach($cards as $card)
			$encryptedCards[] = $Player->encrypt($Request, $card);
		return $encryptedCards;
	}

	public function getBoardCards() {
		if(!$this->mDeck->hasGroup(self::GROUP_BOARD))
			return array();
		return $this->mDeck->getDrawnCards(self::GROUP_BOARD);
	}

	public function check(IRequest $Request, $playerID) {
		$this->writeLine($Request, self::EVENT_CHECK, $playerID);
	}

	protected function writeLine(IRequest $Request, $command, $_arg) {
		$args = func_get_args();
		array_shift($args);
		$this->readLine($Request, $args);

		$logPath = sprintf(HoldemTable::PATH_FORMAT, $this->getTableID()) . HoldemTable::ROUND_LOG;
		$path = Config::getContentPath($logPath);
		$handle      = fopen($path, 'a');
		if(!$handle) {
			if(!is_dir(dirname($path)))
				mkdir(dirname($path), 0777, true);
			$handle      = fopen($path, 'a');
			if(!$handle)
				throw new \Exception("Could not write to file: " . $path);
		}
		$l = 0;
		foreach($args as $i => $arg) {
			if($i > 0) $l+=fwrite($handle, ' ');
			$l+=fwrite($handle, $arg);
		}
		fwrite($handle, "\n");
		fclose($handle);

		if($Request instanceof SocketRequest) {
			/** @var WebSocket $App */
			$App = $Request->getSocket()->getClientApplication();
			$App->checkFile($logPath);
		}
	}

	public function executePendingDealerAction(IRequest $Request) {
		$playerCount = sizeof($this->getPlayers());
		if($playerCount < 2)
			throw new \InvalidArgumentException("Dealer is waiting for 2 or more players");

		if(!$this->mButtonID) {
			$Players = $this->getPlayers();
			$this->log("Auto-assigning dealer button to first player", $this::ERROR);
			$this->mButtonID = $Players[0]->getPlayerID();
			$this->writeLine($Request, self::EVENT_BUTTON, $this->mButtonID);
		}

		$Dealer = $this->getDealer();

		$boardCount = $this->mDeck->getCardCount(self::GROUP_BOARD);

		if($this->mDeck->getDrawCount() === 0) {

			list($smallBlind, $bigBlind) = $this->getTable()->getBlinds();
			if($this->getSmallBlindPlayer()->getBetAmount() < $smallBlind)
				$this->bet($Request, $this->getSmallBlindPlayer()->getPlayerID(), $smallBlind);
			if($this->getBigBlindPlayer()->getBetAmount() < $bigBlind)
				$this->bet($Request, $this->getBigBlindPlayer()->getPlayerID(), $bigBlind);

			for($i=0; $i<2; $i++) {
				foreach ($this->getPlayers() as $Player) {
					if (!$Player->hasPair()) {
						if ($Player->hasFolded())
							continue;
						$card = $this->mDeck->draw($Player->getPlayerID());
						$encryptedCard = $Player->encrypt($Request, $card, $Dealer->getFingerprint());
						$signedCard = $this->getDealer()->sign($Request, $card);
						$this->getDealer()->verify($Request, $card, $signedCard);
						$this->writeLine($Request, self::EVENT_DEAL, $Player->getPlayerID(), base64_encode($encryptedCard), base64_encode($signedCard));
					}
				}
			}
			return false;
		}

		// Check for player moves

		if($waitingPlayerID = $this->waitingForPlayerID())
			throw new \InvalidArgumentException("Waiting on player: " . $waitingPlayerID);

		// Check for player all-in
		$curBet = $this->getCurrentBetAmount();
		foreach($this->getPlayers() as $Player) {
			if ($Player->isAllIn() && $Player->getBetAmount() < $curBet) {
				if (!$this->getSidePot($Player->getPlayerID())) {

					$lowBet = $Player->getBetAmount();
					$sidePot = 0;
					foreach($this->getPlayers() as $Player2) {
						$sideAmount = $Player2->getBetAmount();
						if($sideAmount > $lowBet)
							$sideAmount = $lowBet;
						$sidePot += $sideAmount;
					}
					$this->writeLine($Request, self::EVENT_SIDE_POT, $Player->getPlayerID(), $sidePot);
					$this->log("Dealer sets side pot at {$sidePot} for player " . $Player->getPlayerID());
				}
			}
		}

		// Work the board

		$ActivePlayers = array();
		foreach($this->getPlayers() as $Player) {
			if(!$Player->hasFolded() && !$Player->isAllIn())
				$ActivePlayers[] = $Player;
		}

		if(sizeof($ActivePlayers) === 1) {
			$this->endRound($Request);
			return true;
		}

		if($boardCount >= 5) {
			$this->endRound($Request);
			return true;
		}

		$this->log("Dealer burns card");
		$card = $this->mDeck->draw(self::GROUP_BURN);
		$encryptedDealerCard = $this->getDealer()->encrypt($Request, $card);
		$signedCard = $this->getDealer()->sign($Request, $card);
		$this->writeLine($Request, self::EVENT_DEAL, self::GROUP_BURN, base64_encode($encryptedDealerCard), base64_encode($signedCard));

		if($boardCount < 3) {
			for($i=$boardCount; $i<3; $i++) {
				$card = $this->mDeck->draw(self::GROUP_BOARD);
				$signedCard = $this->getDealer()->sign($Request, $card);
				$this->writeLine($Request, self::EVENT_DEAL, self::GROUP_BOARD, $card, base64_encode($signedCard));
			}
			$this->log("Dealer deals flop: " . Cards::printCards($this->mDeck->getDrawnCards(self::GROUP_BOARD)));

		} elseif ($boardCount < 4) {
			$card = $this->mDeck->draw(self::GROUP_BOARD);
			$signedCard = $this->getDealer()->sign($Request, $card);
			$this->writeLine($Request, self::EVENT_DEAL, self::GROUP_BOARD, $card, base64_encode($signedCard));
			$this->log("Dealer deals river: " . Cards::printCards($this->mDeck->getDrawnCards(self::GROUP_BOARD)));

		} elseif ($boardCount < 5) {
			$card = $this->mDeck->draw(self::GROUP_BOARD);
			$signedCard = $this->getDealer()->sign($Request, $card);
			$this->writeLine($Request, self::EVENT_DEAL, self::GROUP_BOARD, $card, base64_encode($signedCard));
			$this->log("Dealer deals turn: " . Cards::printCards($this->mDeck->getDrawnCards(self::GROUP_BOARD)));
		}
		return false;
	}

	public function hasPlayer($playerID) {
		return $this->getTable()->hasPlayer($playerID);
	}

	public function getPlayer($playerID) {
		return $this->getTable()->getPlayer($playerID);
	}

	/**
	 * @return HoldemPlayer[]
	 */
	public function getPlayers() {
		return $this->getTable()->getPlayers();
	}

	public function getDealer() {
		return $this->getTable()->getDealer();
	}

	protected function readLine(IRequest $Request, $args) {
		switch($args[0]) {
//			case self::EVENT_BLINDS:
//				list(, $smallBlind, $bigBlind) = $args;
//				$this->mBigBlind = (int)$bigBlind;
//				$this->mSmallBlind = (int)$smallBlind ?: $smallBlind;
//				break;

			case self::EVENT_BUTTON:
				list(, $playerID) = $args;
				$this->getPlayer($playerID);
				$this->mButtonID = $playerID;
				break;

			case self::EVENT_DEAL:
				list(, $playerID, $card, $signedCard) = $args;
				if(!is_numeric($card))
					$card = (int)$this->getDealer()->decrypt($Request, base64_decode($card));
				else
					$card = (int)$card;
				if($signedCard) // TODO: remove live
					$this->getDealer()->verify($Request, $card, base64_decode($signedCard));
				$this->mDeck->replay($card, $playerID);

				foreach($this->getPlayers() as $Player)
					$Player->clearCheckAndBet();

				switch($playerID) {
					case self::GROUP_BOARD:
					case self::GROUP_BURN:
						break;
					default:
						$Player = $this->getPlayer($playerID);
						$Player->deal($card);
						break;
				}
				break;

			case self::EVENT_CHECK:
				list(, $playerID) = $args;
				$Player = $this->getPlayer($playerID);
				if($Player->getBetAmount() < $this->getCurrentBetAmount())
					throw new \Exception("Please bet, call or fold. Player may not check at this time");
				$Player->check();
				break;

			case self::EVENT_FOLD:
				list(, $playerID, $card1, $card2) = $args + array(null, null, null, null);
				$Player = $this->getPlayer($playerID);
				$Player->fold();
				break;

			case self::EVENT_CALL:
				list(, $playerID, $amount) = $args;
				$Player = $this->getPlayer($playerID);
				$Player->callToAmount($amount);
				break;

			case self::EVENT_ALLIN:
				list(, $playerID, $amount) = $args;
				$Player = $this->getPlayer($playerID);
				$Player->allInToAmount($amount);
				break;

			case self::EVENT_BET:
				list(, $playerID, $betAmount) = $args;
				$Player = $this->getPlayer($playerID);
				$Player->betAmount($betAmount);
				break;

			case self::EVENT_RAISE:
				list(, $playerID, $raiseAmount) = $args;
				$betToAmount = $this->getCurrentBetAmount() + $raiseAmount;
				$Player = $this->getPlayer($playerID);
				$Player->raiseToAmount($betToAmount);
				break;

			case self::EVENT_SIDE_POT:
				list(, $playerID, $sidePot) = $args;
				$this->getPlayer($playerID);
				$this->mSidePots[$playerID] = $sidePot;
				break;

			case self::EVENT_ROUND_BEGIN:
				$this->mStarted = true;
				$this->mDeck = new Deck();
				$this->mOffset = filesize($this->getLogPath($this->getTableID()));

				foreach($this->getPlayers() as $Player)
					$Player->reset();
				break;
//
//			case self::EVENT_WIN:
//				list(, $playerID, $amount) = $args;
//				break;

//			case self::EVENT_LOSE:
//				list(, $playerID, $amount) = $args;
//				break;

			case self::EVENT_READY:
				list(, $playerID) = $args;
				$this->getPlayer($playerID)->ready();
				break;

			case self::EVENT_MESSAGE:
			case self::EVENT_WIN_HAND:
				break;

			default:
				if($args) {
					throw new \InvalidArgumentException("Unknown event: " . $args[0]);
				}
		}
	}


	/**
	 * Add a log entry
	 * @param mixed $msg The log message
	 * @param int $flags [optional] log flags
	 * @return int the number of listeners that processed the log entry
	 */
	function log($msg, $flags = 0) {
		$c = 0;
		foreach ($this->mLogListeners as $Log)
			$c += $Log->log($msg, $flags);

		return $c;
	}

	/**
	 * Add a log listener callback
	 * @param ILogListener $Listener
	 * @return void
	 * @throws \InvalidArgumentException if this log listener inst does not accept additional listeners
	 */
	function addLogListener(ILogListener $Listener) {
		if (!in_array($Listener, $this->mLogListeners))
			$this->mLogListeners[] = $Listener;
	}

	function startNextRound(IRequest $Request) {
		if($this->hasStarted())
			throw new \InvalidArgumentException("Round already started");

		foreach($this->getPlayers() as $Player) {
			list(, $bigBlind) = $this->getTable()->getBlinds();
			if($Player->getBalance() < $bigBlind) {
				$this->log("Player has insufficient funds to continue. Removing player: " . $Player->getPlayerID());
				$this->getTable()->stand($Request, $Player->getPlayerID());
			}
		}

		if(!$this->mButtonID) {
			$Players = $this->getPlayers();
			$this->log("Auto-assigning dealer button to first player", $this::ERROR);
			$this->mButtonID = $Players[0]->getPlayerID();
			$this->writeLine($Request, self::EVENT_BUTTON, $this->mButtonID);
		}

		$this->getDealerButtonPlayer($p);
		$Players = $this->getPlayers();
		if(++$p >= sizeof($Players))
			$p = 0;
		$this->setDealerButton($Request, $Players[$p]->getPlayerID());

		$this->log("Dealer starts the round");
		$this->writeLine($Request, self::EVENT_ROUND_BEGIN, $this->mOffset);

		$this->executePendingDealerAction($Request);
	}

	function endRound(IRequest $Request) {
		$Players = array();
		foreach($this->getPlayers() as $Player) {
			if($Player->hasFolded())
				continue;
			$Players[] = $Player;
		}
		if(sizeof($Players) === 0)
			throw new \Exception("All players have folded");

		if(sizeof($Players) > 1) {
			$WinnerGroups = $this->chooseWinners($Players);

		} else {
			$WinnerGroups = array(
				0 => $Players
			);
		}

		foreach($WinnerGroups as $i => $Winners) {
			$betTotal = $this->getCurrentPotBalance() - $this->mPaidOut;
			if(!$betTotal) {
				for($j=$i; $j<sizeof($WinnerGroups); $j++) {
					$Losers = $WinnerGroups[$j];
					foreach ($Losers as $Loser) {
						/** @var HoldemPlayer $Loser */
						$this->mWhoWon[] = array(HoldemTable::EVENT_LOSE, $Loser->getPlayerID(), $Loser->getBetAmount());
					}
				}
				continue;
			}

			$c = sizeof($Winners);
			$split = floor($betTotal / $c);
			$remainder = $betTotal % $c;

			$winnerList = array();
			foreach($Winners as $Winner) {
				$cards = $this->mDeck->getDrawnCards($Winner->getPlayerID(), $this->mDeck->hasGroup(self::GROUP_BOARD) ? self::GROUP_BOARD : null);
				$Hands = PokerHands::getAllHands($cards);
				$winnerList[] = $Winner->getPlayerID() . ' with ' . $Hands[0] . ' (' . $Hands[0]->getRank() . ')';

				$amount = $split;
				if($sidePot = $this->getSidePot($Winner->getPlayerID()))
					if($sidePot < $split)
						$amount = $sidePot;

//				$amount -= $Winner->getBetAmount();

				$total = $this->getCurrentPotBalance();
				$total -= $this->mPaidOut;
				if($total < $amount)
					throw new \InvalidArgumentException("Invalid pay out balance");
				$this->mWhoWon[] = array(HoldemTable::EVENT_WIN, $Winner->getPlayerID(), $amount - $Winner->getBetAmount());
				$this->mPaidOut += $amount;

				foreach($Hands as $Hand)
					$this->writeLine($Request, self::EVENT_WIN_HAND, $Winner->getPlayerID(), str_replace(' ', '_', $Hand->getName()), implode(' ', $Hand->getCards(true)));

				$this->log("Dealer pays out {$amount} to " . $Winner->getPlayerID());

			}
			$this->log("Dealer announces " .($i>0 ? 'next ' : '') . "winner" . (sizeof($winnerList)>1 ? 's' : '') . ": " . implode(', ', $winnerList));

			if($remainder){
				$this->mWhoWon[] = array(HoldemTable::EVENT_WIN, $Winners[0]->getPlayerID(), $remainder);
				$this->mPaidOut += $remainder;
				$this->log("Dealer pays out remainder of {$remainder} to " . $Winners[0]->getPlayerID() . " ya know, because.");
			}
		}

		$this->mStarted = false;
	}

	public function getSidePot($playerID) {
		if(isset($this->mSidePots[$playerID]))
			return $this->mSidePots[$playerID];
		return false;
	}

	function getMaximumBet($playerID) {
		$max = $this->getPlayer($playerID)->getBalance();
		foreach ($this->getPlayers() as $Player) {
			if (!$Player->hasFolded()) {
				if ($Player->getBalance() < $max) {
					$max = $Player->getBalance();
				}
			}
		}

		return $max;
	}

	public function compareHands(HoldemPlayer $Player1, HoldemPlayer $Player2) {
		$Deck = $this->mDeck;
		$hasBoard = $Deck->hasGroup(self::GROUP_BOARD);
		$cards1 = $Deck->getDrawnCards($Player1->getPlayerID(), $hasBoard ? self::GROUP_BOARD : null);
		$cards2 = $Deck->getDrawnCards($Player2->getPlayerID(), $hasBoard ? self::GROUP_BOARD : null);
		return PokerHands::compareHands($cards2, $cards1);
	}

	/**
	 * @param HoldemPlayer[] $Players
	 * @throws \Exception
	 * @return HoldemPlayer[][]
	 */
	public function chooseWinners(Array $Players) {
		$THIS = $this;
		usort($Players,
			function(HoldemPlayer $Player1, HoldemPlayer $Player2) use ($THIS) {
				return $THIS->compareHands($Player1, $Player2);
			}
		);

		/** @var HoldemPlayer[] $Winners */
		$WinnerGroups = array(0=>array());
		$winnerGroup = 0;

		foreach($Players as $i => $Player) {
			if(isset($Players[$i+1])) {
				$compare = $this->compareHands($Player, $Players[$i+1]);
				if($compare > 0) {
					throw new \Exception("Invalid winner hand value"); }
				$WinnerGroups[$winnerGroup][] = $Player;
				if($compare < 0) {
					$winnerGroup++;
				}
			} else {
				$WinnerGroups[$winnerGroup][] = $Player;
			}
		}
		return $WinnerGroups;
	}

	// Static

	static function getLogPath($tableID) {
		return Config::getContentPath(sprintf(self::PATH_FORMAT, $tableID));
	}

	/**
	 * Perform a unit test
	 * @param IUnitTestRequest $Test the unit test request inst for this test session
	 * @return void
	 * @test --disable 0
	 * Note: Use doctag 'test' with '--disable 1' to have this ITestable class skipped during a build
	 */
	static function handleStaticUnitTest(IUnitTestRequest $Test) {
		$Table = new HoldemTable($Test, 'test-table');
		unlink($Table->getRequestPath() . $Table::TABLE_LOG);
		unlink($Table->getRequestPath() . $Table::ROUND_LOG);
		$Table = new HoldemTable($Test, 'test-table');
		$Table->setBlinds($Test, 5, 10, 'test');
		$Round = $Table->getRound();

		/** @var HoldemTable $Table */
		$Round->addLogListener($Test);
		$Table->addLogListener($Test);
		$Table->setDealer($Test, 'test-dealer');

		/** @var TestHoldemPlayer[] $TestUser */
		for($i=1; $i<5; $i++)
			$TestUser[$i] = new TestHoldemPlayer($Test, $i);

		/** @var HoldemPlayer[] $Player */
		$Player = array();
		for($i=1; $i<5; $i++) {
			$Table->sit($Test, $TestUser[$i]->getUserID(), 1000 * $i, $TestUser[$i]->getFingerprint());
			$Player[$i] = $Round->getPlayer($TestUser[$i]->getUserID());
			$Round->ready($Test, $Player[$i]->getPlayerID());
		}

		$buttonID = array_rand($Player);
		$Round->setDealerButton($Test, $Player[$buttonID]->getPlayerID());
		$Round->startNextRound($Test);

		for($j=1; $j<5; $j++)
			foreach($TestUser as $User)
				$User->doAction($Test, $Round);
		$Round->executePendingDealerAction($Test);

		for($j=1; $j<5; $j++)
			foreach($TestUser as $User)
				$User->doAction($Test, $Round);
		$Round->executePendingDealerAction($Test);

		for($j=1; $j<5; $j++)
			foreach($TestUser as $User)
				$User->doAction($Test, $Round);
		$Round->executePendingDealerAction($Test);

		for($j=1; $j<5; $j++)
			foreach($TestUser as $User)
				$User->doAction($Test, $Round);
		$Round->executePendingDealerAction($Test);

	}
}

if(!empty($argv) && $argv[0] === __FILE__)
	HoldemRound::handleStaticUnitTest(new UnitTestRequestWrapper(new CLIRequest('GET', '')));